문단 클릭 = 시작 위치 선택 · 재생은 우하단 버튼/P (재생 중 클릭 = 점프)

1장. 랭체인이라는 레고 상자

출처: 『RAG 마스터: 랭체인으로 완성하는 LLM 서비스』(브라이스 유·조경아·박수진·김재웅 지음, 프리렉 2025) | 공식: www.langchain.com

코드는 분위기만 — pip install·|·invoke 같은 말은 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.

이 장은 0장에서 배운 부품(LLM·검색기·임베딩)을 손으로 끼워 맞추게 해 주는 도구, 랭체인을 본다.

레고 상자를 처음 열듯, 어떤 블록이 들어 있고 그 블록을 어떻게 잇는지 천천히 살핀다.


0. 이 장의 새 단어 (0장에 없는 것만 3개)

0장 용어집에 있는 말(LLM·RAG·임베딩·체인·프롬프트·토큰·온도 등)은 다 안다고 보고 간다.

이 장에서 처음 나오는 말은 딱 3개다. 먼저 풀어 둔다.


러너블(runnable)

한 문장 뜻 — 랭체인에서 실행할 수 있는 작업 한 칸. 프롬프트도, 모델도, 파서도 전부 이 한 칸으로 똑같이 생겼다.

일상비유 — 같은 규격의 레고 블록. 바퀴든 문이든 밑면 돌기가 같으니, 무엇이든 끼우면 붙는다.

한 줄 예 —

# 프롬프트도 모델도 파서도 다 "러너블" → 같은 invoke로 부른다
prompt.invoke({"topic": "더블딥"})  # 프롬프트 한 칸 실행
model.invoke("안녕")               # 모델 한 칸 실행

출력 파서(output parser)

한 문장 뜻 — 모델이 뱉은 줄글을 프로그램이 쓰기 좋은 정리된 모양(JSON·표 등)으로 바꿔 주는 부품.

일상비유 — 손님이 말로 늘어놓은 주문을 주방용 주문표 칸에 또박또박 옮겨 적는 점원. 말은 자유롭지만, 표는 칸이 정해져 있다.

한 줄 예 —

# 모델의 줄글 답을 그냥 문자열로 깔끔히 받는다
parser = StrOutputParser()

메모리(memory, 대화 기록)

한 문장 뜻 — 챗봇이 방금 전 대화를 기억하게 해, 앞 내용을 이어 말하게 하는 장치.

일상비유 — 통화 중 메모지. "아까 뭐라고 했죠?" 물으면 메모지를 보고 답한다. 메모지가 없으면 매번 처음 본 사람처럼 군다.

한 줄 예 —

# 앞 대화를 적어 두고 다음 질문에 같이 넘긴다
history.add_user_message("저축 어떻게 늘려요?")

(귀납 도입) OpenAI만 쓰다가 모델을 바꾸려니 코드 절반이 터졌다

처음엔 OpenAI 한 줄로 충분했다.

import openai
client = openai.OpenAI()
client.chat.completions.create(
    model="gpt-4o-mini",
    # role=누가 말했나(user=사용자), content=무슨 말인가 — 이 짝 하나가 대화 한 줄
    messages=[{"role": "user", "content": "안녕하세요!"}]
)

잘 돌아간다. 그래서 회사 곳곳에 이 호출을 흩뿌렸다.

그런데 어느 날 "Gemini가 더 싸다, 갈아타자" 하는 말이 나왔다.

문제가 터진다. OpenAI 호출법과 Gemini 호출법은 생김새가 다르다. 흩뿌려 둔 그 많은 호출을 하나하나 뜯어고쳐야 한다.

모델 하나 바꾸려다 코드 절반을 다시 짠다.

이 고생을 줄이려고 나온 게 랭체인이다.

랭체인은 모델을 표준 규격으로 한 번 감싼다. 그러면 모델 교체가 블록 하나 갈아 끼우는 일로 줄어든다.

# 랭체인 — 모델은 클래스 한 줄. 교체도 한 줄.
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
# 다른 모델로? 윗줄만 ChatMistralAI(...) 로 바꾸면 끝.

이 장은 그 "감싸고 끼우는" 방식을 처음부터 본다.


이 장에서 딱 6가지만

  1. 랭체인은 한 덩어리가 아니라 부품 상자(생태계)다. core·langchain·community·파트너·langgraph 등 역할별 블록이 들어 있다.
  2. 랭체인은 모델을 직접 만들지 않는다. OpenAI·Gemini 같은 남의 모델을 표준 규격으로 감싸, 같은 방식으로 부르게 해 준다.
  3. LCEL은 블록을 |(파이프)로 잇는 방식이다. 프롬프트 → 모델 → 파서를 한 줄로 줄 세운다.
  4. 프롬프트 템플릿은 빈칸 양식이다. 빈칸에 값만 갈아 끼워 모델에게 지시를 만든다. 예시를 몇 개 보여 주는 게 퓨샷.
  5. 출력 파서는 모델의 줄글을 정리된 모양으로 바꾼다. JSON·검증된 데이터 등.
  6. 메모리는 챗봇이 앞 대화를 기억하게 한다. 그냥 넘기기 → 차곡차곡 저장 → 자동 관리 → 길면 줄이거나 요약.

여섯 블록을 하나씩 본다. 막히면 0장 용어집으로 돌아가면 된다.


개념 1 — 랭체인은 부품 상자(생태계)다

망가지는 장면

"랭체인 깔았으니 다 되겠지" 하고 한 패키지만 믿었다.

그런데 어떤 기능은 다른 패키지에 있고, 어떤 건 선택 설치다.

뭐가 어디 있는지 모르면, 필요한 블록을 못 찾아 헤맨다.

일상비유

랭체인은 부품이 칸칸이 나뉜 공구 상자다.

드라이버 칸, 나사 칸, 특수 공구 칸이 따로 있다. 칸 이름만 알면 필요한 걸 바로 꺼낸다.

비유 코드 위험
바닥에 깔리는 기본 받침 import langchain_core 없으면 위 블록들이 못 선다
그 위에 얹는 본체 import langchain 깔면 core가 자동으로 같이 깔림
선택 추가 통합 모음 import langchain_community 안 깔면 일부 외부 연결 불가

한 문장 정의 — 랭체인은 하나의 패키지가 아니라, 역할별로 나뉜 여러 패키지를 조합해 쓰는 생태계다.

주요 블록(패키지)을 한눈에

블록 이름 무슨 칸인가
langchain-core 상자 바닥. 모델·검색기·파서의 기본 규격과 LCEL을 정한다. 가볍다.
langchain 본체. 체인·에이전트 같은 앱 뼈대를 만든다. 깔면 core가 함께 깔린다.
langchain-community 커뮤니티가 챙기는 외부 연결 모음. 필요할 때만 선택 설치.
파트너 패키지 자주 쓰는 연결만 따로 뺀 것. langchain-openai·langchain-anthropic 등.
langgraph 갈림길·되돌이가 있는 복잡한 흐름을 그래프로 그릴 때.
langserve 만든 체인을 웹 API로 내보낼 때.
langsmith 잘 도는지 들여다보고 점검할 때(디버깅·모니터링).

단계별 예시 — 의존성 화살표 읽기

(worked) 화살표 "A → B"는 "A가 B에 기댄다"는 뜻이다.

langchain → langchain-core 이므로, langchain만 깔아도 core가 딸려 온다.

# langchain만 깔아도 core가 함께 깔린다
# (직접 의존성: langchain이 core에 직접 기댐)
!pip install langchain
import langchain_core  # 따로 안 깔았는데도 쓸 수 있음

(부분 완성) langgraph는 core를 피어 의존성으로 쓴다. 즉 "있으면 골라 쓰는" 사이다.

빈칸을 채워 보자. langgraph는 core를 ____(필수로 / 골라서) 쓴다. → 답: 골라서.

(독립 적용) 친구가 "langchain-anthropic은 뭐예요?" 물으면?

→ "자주 쓰는 외부 연결(앤트로픽 Claude)만 따로 뺀 파트너 패키지예요. 필요할 때만 깔면 됩니다."

버전 이야기 (1.2)

랭체인은 버전이 자주 오른다. 큰 줄기만 잡으면 된다.

  • 0.1 — 패키지를 잘게 나눔(분리 시작).
  • 0.2 — community 의존성을 떼어 더 가벼워짐.
  • 0.3(2024년 9월) — 내부를 Pydantic 2로 바꾸고, Python 3.8 지원을 끝냄.

버전은 자주 바뀌어도 전체 구조는 거의 그대로다. 새 버전이 나오면 바뀐 점만 빠르게 보면 된다.

왜 굳이 랭체인인가 (1.3)

OpenAI만으로도 앱은 만든다. 그래도 랭체인을 쓰는 이유 4가지.

장점 한 줄 뜻
모듈성 기능을 블록으로 끼웠다 뺐다 한다
통합 용이성 OpenAI → Gemini 교체를 설정만 바꿔 한다
확장 기능 LCEL·러너블로 복잡한 흐름을 짧게 쓴다
커뮤니티 예제·문서가 많고 자주 업데이트된다

무엇에 쓰나 (1.4) — RAG와 질의응답, 구조화 출력 뽑기, 챗봇, 도구 사용·에이전트. 이 책의 각 장이 이 쓰임을 하나씩 다룬다.

한 걸음 더 ▸ (지금 몰라도 됨) — 패키지가 많아 보여도, 처음엔 langchain·langchain-core·langchain-openai 세 개면 충분하다. 나머지는 필요할 때 하나씩 꺼내 쓰면 된다.


개념 2 — 모델은 직접 안 만들고, 감싸서 갈아 끼운다

망가지는 장면

OpenAI 호출법으로 코드를 가득 채웠다.

모델을 바꾸려니 호출법이 달라, 곳곳을 다 뜯어고친다.

도입부의 바로 그 고생이다.

일상비유

여러 나라 플러그를 한 모양으로 맞춰 주는 멀티 어댑터.

벽 콘센트가 무엇이든, 어댑터 너머는 늘 같은 모양이라 기기를 그대로 꽂는다.

비유 코드 위험
어댑터 없이 직접 꽂기 client.chat.completions.create(model=...) 모델 바꾸면 곳곳 수정
어댑터로 감싸 꽂기 ChatOpenAI(model="gpt-4o") 교체는 클래스 한 줄만

한 문장 정의 — 랭체인은 LLM을 직접 만들지 않고, OpenAI·Cohere·Hugging Face 같은 여러 제공자를 표준 인터페이스로 감싸 똑같이 다루게 해 준다.

단계별 예시 — 직접 호출 vs 랭체인

(worked) 잘못된 결(특정 모델에 딱 붙음):

# OpenAI 직접 — 모델 교체 시 호출부를 다 고쳐야
import openai
client = openai.OpenAI()
client.chat.completions.create(
    model="gpt-4o-mini",
    # 인자 이름부터 호출 모양까지 OpenAI 전용 — 다른 회사 모델은 이 모양을 못 알아듣는다
    messages=[{"role": "user", "content": "안녕하세요!"}]
)

(worked) 올바른 결(블록 하나만 갈면 됨):

# 감싼 버전 — 모델을 바꿔도 아래 chain·invoke 줄은 손대지 않는다
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_template("주제 {topic}에 대해 짧은 설명을 해주세요.")
model = ChatOpenAI(model="gpt-4o")
# 미스트랄로? 윗줄만 ChatMistralAI(...) 로.
# | 는 "왼쪽 결과를 오른쪽에 넘겨라"는 이음표 — 개념 3에서 자세히 본다
chain = prompt | model | StrOutputParser()
chain.invoke({"topic": "더블딥"})

(부분 완성) 두 방식을 표로 비교해 빈칸을 채워 보자.

항목 OpenAI 직접 랭체인
구조 단순 호출 모듈화된 체인
모델 전환 코드 수정 필요 ____
적합 간단한 작업 복잡·확장 앱

→ 빈칸: 클래스만 교체.

(독립 적용) "간단한 스크립트 하나, 모델 바꿀 일 없음" — 이럴 땐? → 굳이 랭체인 없이 OpenAI 직접 호출도 충분하다. 도구는 일에 맞춰 고른다.

모델의 손잡이들 (파라미터 2.2)

같은 모델도 손잡이를 돌려 답의 결을 바꾼다.

손잡이 무엇을 조절
temperature(0~1) 낮으면 또박또박 일관, 높으면 톡톡 튀게
max_tokens 답 길이의 상한
top_p 상위 확률 단어만 고르기
frequency / presence penalty 반복 줄이기 / 새 단어 장려
stop sequences 특정 글자 만나면 생성 멈추기
# 온도 낮춤 = 매번 비슷한 답, 길이는 100조각까지
from langchain_openai import OpenAI
llm = OpenAI(temperature=0.2, max_tokens=100)

어떤 모델을 고르나 (2.3) — 제공자마다 문맥 크기(한 번에 읽는 글 양)·백만 토큰당 입출력 비용·한국어 성능이 다르다.

한국어 성능은 LogicKor 같은 대시보드로 비교한다.

비용과 모델은 수시로 바뀌므로, 쓸 때 공식 가격표를 꼭 확인한다.

한 걸음 더 ▸ — 책의 gpt-4o·Claude 3.5·Gemini는 2025년 기준 예시다. 실제로 쓸 땐 공식 모델 목록을 본다: OpenAI·Anthropic·Google. (검증 2026-05-21)


개념 3 — LCEL: 블록을 |로 한 줄에 줄 세운다

망가지는 장면

프롬프트 만들고, 모델 부르고, 답을 정리하고… 단계가 많다.

이걸 하나하나 손으로 이으면, 중간에 데이터 모양이 안 맞아 자꾸 어긋난다.

일상비유

컨베이어 벨트. 재료를 올리면 자르고 → 굽고 → 포장이 차례로 흐른다.

각 칸은 앞 칸이 넘긴 걸 받아 처리하고 다음 칸에 넘긴다. 사람이 일일이 들고 옮기지 않는다.

비유 코드 위험
벨트로 칸 잇기 chain = prompt \| model \| parser 앞 칸 출력 모양이 다음 칸 입력과 안 맞으면 막힘
벨트에 재료 올리기 chain.invoke({"topic": "더블딥"}) 입력 키 이름 틀리면 빈칸 안 채워짐

한 문장 정의 — LCEL(랭체인 표현 언어)은 러너블 블록을 파이프(|)로 이어, "무엇을 할지"를 선언적으로 한 줄에 쓰는 방식이다.

선언적이란? — "어떻게 하나하나 하라"가 아니라 "무엇을 할지"만 적는 것이다.

벨트 설계도만 그려 두면, 재료가 알아서 흐르는 식이다.

모든 블록이 공유하는 공통 버튼 (러너블 표준 메서드 3.1)

프롬프트·모델·파서가 다 러너블이라, 같은 버튼으로 부른다.

동기 비동기 무슨 일
invoke() ainvoke() 입력 하나 처리
batch() abatch() 여러 입력 한꺼번에
stream() astream() 답을 토막토막 실시간으로
# 같은 체인, 부르는 버튼만 다름
chain.invoke({"topic": "더블딥"})                       # 하나
chain.batch([{"topic": "더블딥"}, {"topic": "인플레이션"}])  # 여럿
for token in chain.stream({"topic": "더블딥"}):          # 토막 실시간
    # end="" 는 토막들을 줄바꿈 없이 한 줄로 이어 붙인다 — 빼면 토막마다 줄이 바뀐다
    print(token, end="", flush=True)

참고stream()은 답이 다 만들어지기 전에 토막을 흘려보낸다. flush=True는 "화면에 바로바로 보여라"라는 뜻이다. 채팅창에 글자가 또르륵 찍히는 그 느낌이다.

단계별 예시 — 블록 잇는 여러 방법 (3.2)

(worked) 파이프 연산자 — 가장 흔한 방법:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_template("주제 {topic}에 대해 짧은 설명을 해주세요.")
parser = StrOutputParser()
chain = prompt | model | parser   # 이 | 가 "이어 붙여라"
chain.invoke({"topic": "더블딥"})

참고 — |는 어떻게 작동하나 — 파이썬은 원래 |로 이런 연결을 안 해 준다. 랭체인이 __or__라는 특별 약속을 덧씌워서(오버로딩) "|를 만나면 블록을 이어라"로 바꿔 둔 것이다. 지금은 "랭체인이 |에 새 뜻을 줬다"만 알면 된다.

(worked) .pipe() 메서드 — 같은 일을 메서드로:

# prompt | model | parser 와 같은 결과
chain = prompt.pipe(model).pipe(parser)

(worked) 체인 뒤에 체인 잇기 — 설명을 만들고 영어로 번역까지:

# {"answer": chain} — 앞 chain의 결과에 "answer"라는 이름표를 붙인다
# 이 이름표가 다음 프롬프트의 {answer} 빈칸과 같아야 답이 그 자리에 들어간다
analysis_prompt = ChatPromptTemplate.from_template("이 대답을 영어로 번역해 주세요: {answer}")
composed = {"answer": chain} | analysis_prompt | model | StrOutputParser()
composed.invoke({"topic": "더블딥"})  # 더블딥 설명을 영어로 받음

(부분 완성) 중간에 손수 만든 함수도 블록으로 끼울 수 있다. 함수는 러너블로 자동 변환된다.

# chain 결과를 {"answer": ...} 모양으로 바꿔 다음에 넘기는 람다
composed = chain | (lambda x: {"answer": x}) | analysis_prompt | model | StrOutputParser()

빈칸: 단, 이렇게 람다를 끼우면 ____ 작업과 호환이 안 될 수 있어 주의한다. → 답: 스트리밍.

(독립 적용) "같은 주제를 한국어·영어로 동시에 받고 싶다." → RunnableParallel로 두 체인을 나란히 돌린다.

from langchain_core.runnables import RunnableParallel

kor = ChatPromptTemplate.from_template("{topic}에 대해 짧은 설명을 해주세요.") | model | parser
eng = ChatPromptTemplate.from_template("{topic}에 대해 짧게 영어로 설명을 해주세요.") | model | parser
parallel = RunnableParallel(kor=kor, eng=eng)
result = parallel.invoke({"topic": "더블딥"})
# result["kor"], result["eng"] 로 둘 다 받음

미니 시나리오 — 사용자가 "응답이 너무 느려요. 다 나올 때까지 빈 화면이에요" 한다.

invoke 대신 stream으로 바꿔, 만들어지는 토막을 바로바로 보여 준다. 체감 속도가 확 산다.


개념 4 — 프롬프트 템플릿: 빈칸 양식

망가지는 장면

주제마다 지시문을 통째로 새로 쓴다. "더블딥 설명해 줘", "인플레이션 설명해 줘"…

같은 문장을 매번 복사·수정한다. 실수도 늘고 지저분하다.

일상비유

빈칸 있는 양식 편지. "주제 ____에 대해 설명해 줘"를 만들어 두고, 빈칸에 단어만 갈아 끼운다.

비유 코드 위험
빈칸 양식 만들기 ChatPromptTemplate.from_template("주제 {topic}…") 빈칸 이름과 입력 키가 다르면 안 채워짐
빈칸에 값 끼우기 prompt.invoke({"topic": "주식"}) 딕셔너리로 줘야 함

한 문장 정의 — 프롬프트 템플릿은 사용자 입력을 모델용 지시문으로 바꾸는 빈칸 양식이며, {변수} 자리에 값을 채워 완성한다. 입력은 딕셔너리로 받는다.

세 가지 양식

양식 언제
PromptTemplate 단순한 한 줄 문자열 지시
ChatPromptTemplate system/user/ai 역할이 있는 대화형
MessagesPlaceholder 대화 이력처럼 목록을 끼울 빈칸

단계별 예시 — 양식 만들기

(worked) 문자열 양식:

from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate.from_template("주제 {topic}에 대해 짧은 조언을 해주세요.")
prompt.invoke({"topic": "투자"})  # → "주제 투자에 대해 …" 완성

(worked) 챗 양식 — 역할을 나눠 줌:

from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 유능한 금융 조언가입니다."),  # AI 역할 고정
    ("user", "주제 {topic}에 대해 조언을 해주세요"),  # 사용자 요청
])
prompt.invoke({"topic": "주식"})

(부분 완성) 대화 목록을 통째로 끼울 빈칸:

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage

prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 유능한 금융 조언가입니다."),
    MessagesPlaceholder("msgs"),   # 또는 ("placeholder", "{msgs}")
])
prompt.invoke({"msgs": [HumanMessage(content="안녕하세요!")]})

빈칸: MessagesPlaceholder("msgs") 대신 더 짧게 쓰면 ("placeholder", "____") 이다. → 답: {msgs}.

(독립 적용) "예시를 몇 개 보여 주면 모델이 더 잘한다던데?" → 그게 퓨샷이다(아래 4.1).

퓨샷 프롬프트 (4.1)

예시를 보여 주는 개수로 이름이 갈린다.

  • 0개 = 제로샷
  • 1개 = 원샷
  • n개 = 퓨샷(n샷)

예시를 한꺼번에 넣어 주는 게 FewShotPromptTemplate이다.

(worked) 잘못된 결 — 예시를 전부 욱여넣음(토큰 낭비):

from langchain_core.prompts import FewShotPromptTemplate
prompt = FewShotPromptTemplate(
    examples=examples,          # 예시 전부
    # 예시 하나하나를 어떤 글 모양으로 펼칠지 정한 작은 양식
    example_prompt=example_prompt,
    # 예시들 뒤에 붙는 마지막 부분 — {input} 빈칸에 진짜 질문이 들어간다
    suffix="질문: {input}",
    # invoke 때 넘길 딕셔너리의 키 이름 — 위 {input} 빈칸과 이름이 같아야 채워진다
    input_variables=["input"],
)

(worked) 올바른 결 — 예제 선택기로 비슷한 예시만 골라 넣음(토큰 절약):

from langchain_chroma import Chroma
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_openai import OpenAIEmbeddings

selector = SemanticSimilarityExampleSelector.from_examples(
    examples,
    OpenAIEmbeddings(api_key=api_key),  # 예시를 숫자로 바꿔
    Chroma,                              # 창고에 넣고 비교
    k=1,                                 # 가장 비슷한 1개만 고름
)

미니 시나리오 — 예시가 50개라 매번 다 넣으니 비용이 폭발한다.

→ 예제 선택기에 k=1을 줘, 질문과 가장 가까운 한 개만 골라 넣는다. 비용이 뚝 떨어진다.

프롬프트 허브 (4.2)

남이 잘 만든 프롬프트를 가져다 쓰는 중앙 창고다.

# 허브 기능은 랭체인 1.0부터 별도 패키지로 분리됨: pip install langchain-classic
from langchain_classic import hub
prompt = hub.pull("hardkothari/prompt-maker")        # 최신 버전 가져오기
prompt = hub.pull("hardkothari/prompt-maker:c5db8eee")  # 특정 버전 고정

:c5db8eee 처럼 버전을 콕 집으면, 나중에 원본이 바뀌어도 내가 쓰던 그 버전을 그대로 쓴다.


개념 5 — 출력 파서: 줄글을 정리된 모양으로

망가지는 장면

모델이 "부동산 투자는 장기적으로 안정적이며…" 하고 줄글로 답했다.

이걸 프로그램에서 "질문 부분"과 "답변 부분"으로 나눠 쓰려는데, 줄글이라 칼같이 안 잘린다.

일상비유

손님의 말 주문을 주방 주문표 칸에 옮겨 적는 점원.

말은 자유롭지만, 표는 '메뉴' 칸 '수량' 칸이 정해져 있어 주방이 헷갈리지 않는다.

비유 코드 위험
줄글을 깔끔한 문자열로 StrOutputParser() 구조가 필요하면 부족함
줄글을 칸 있는 표(JSON)로 JsonOutputParser() 모델이 형식 안 지키면 오류

한 문장 정의 — 출력 파서는 모델의 자유 텍스트를 JSON·CSV·검증된 데이터 같은 구조화된 모양으로 바꿔, 프로그램이 곧장 쓰게 해 준다.

참고 — 요즘 모델은 함수·도구 호출을 직접 지원한다. 그쪽이 가능하면 출력 파서 대신 그걸 권한다. 출력 파서는 그게 안 되거나 단순한 경우에 쓴다.

출력 파서가 가진 세 버튼 (5.1)

버튼 무슨 일
get_format_instructions() "이런 형식으로 답해" 지침을 모델에 줄 문구 만들기
parse() 모델 답(줄글·JSON 문자열) → 파이썬 데이터로 변환
parse_with_prompt() 질문까지 함께 받아, 형식 틀렸을 때 고쳐 다시 시도
from langchain_core.output_parsers import JsonOutputParser
parser = JsonOutputParser()
print(parser.get_format_instructions())  # 모델에 줄 형식 지침
# 줄글 JSON을 파이썬 딕셔너리로
parser.parse('{"이름": "김철수", "나이": 30}')  # → {"이름": "김철수", "나이": 30}

단계별 예시 — 파서 종류별로

(worked) PydanticOutputParser — 칸을 정해 두고 검증까지:

from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field, model_validator

class FinancialAdvice(BaseModel):
    # 답의 칸을 미리 정한다 — description 문구는 모델에게 줄 형식 지침에 그대로 들어간다
    setup: str = Field(description="금융 조언 상황 질문")
    advice: str = Field(description="질문에 대한 금융 답변")

    # 아래 두 줄 표식(@)의 뜻: 값을 담기 전에 이 검사 함수를 먼저 통과시켜라
    @model_validator(mode="before")
    @classmethod
    def must_end_with_question(cls, values):
        if not values.get("setup", "").endswith("?"):
            raise ValueError("질문은 '?'로 끝나야 합니다.")  # 형식 어기면 막음
        return values

parser = PydanticOutputParser(pydantic_object=FinancialAdvice)

(worked) SimpleJsonOutputParser — 가벼운 JSON, 스트리밍 지원:

from langchain_core.output_parsers import SimpleJsonOutputParser
json_parser = SimpleJsonOutputParser()
# 값이 한 글자씩 차오르는 걸 실시간으로 볼 수 있음
# {} → {"answer": "비"} → {"answer": "비트"} → …

(부분 완성) JsonOutputParser — JSON 특화. Pydantic 스키마도 결합:

from langchain_core.output_parsers import JsonOutputParser
parser = JsonOutputParser(pydantic_object=FinancialAdvice)
# 결과는 ____ 모양으로 나온다: {"setup": "...", "advice": "..."}

빈칸: 딕셔너리.

(독립 적용) 파서가 너무 많다. 표로 골라 보자.

파서 스트리밍 검증 언제
StrOutputParser 없음 그냥 문자열
SimpleJsonOutputParser 없음 가벼운 JSON
JsonOutputParser 선택 JSON + 스키마
PydanticOutputParser 제한적 있음 형식 검증이 꼭 필요할 때

미니 시나리오 — 모델이 가끔 ? 없이 질문을 만든다.

PydanticOutputParser에 검증 규칙(?로 끝나야 함)을 넣어, 형식 어긴 답을 걸러 낸다.


개념 6 — 메모리: 챗봇이 앞 대화를 기억한다

망가지는 장면

챗봇에게 "저축 어떻게 늘려요?" 묻고 답을 받았다.

이어 "방금 뭐라고 했죠?" 하니, "무슨 말씀이신지 모르겠어요" 한다.

방금 한 말을 전혀 기억 못 한다. 매 질문이 첫 만남이다.

일상비유

통화 중 메모지. 앞에 한 말을 적어 두고, "아까 뭐랬죠?" 하면 메모지를 보고 답한다.

메모지가 없으면 매번 처음 통화하는 사람처럼 군다.

비유 코드 위험
메모지를 같이 건넴 ("placeholder", "{messages}") 안 건네면 앞 대화 까먹음
메모지가 너무 길어짐 트리밍·요약으로 줄임 안 줄이면 느려지고 비싸짐

한 문장 정의 — 메모리는 챗봇이 이전 대화를 기억해 맥락을 이어 가게 하는 장치이며, 그냥 넘기기 → 저장 → 자동 관리 → 줄이기·요약, 점점 더 손이 덜 가게 발전한다.

단계별 예시 — 점점 자동으로 (네 단계)

(worked) 1단계 — 그냥 넘기기(6.1): 앞 대화를 손수 프롬프트 빈칸에 넣는다.

prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 금융 상담사입니다."),
    ("placeholder", "{messages}"),   # 여기에 대화 이력을 통째로
])
chain = prompt | chat
chain.invoke({"messages": [
    ("human", "저축을 늘리려면?"),
    ("ai", "매달 자동이체로 저축하세요."),
    ("human", "방금 뭐라고 했죠?"),   # 앞 두 줄을 같이 줘서 기억함
]})

(worked) 2단계 — ChatMessageHistory(6.2): 대화를 차곡차곡 저장한다.

from langchain_community.chat_message_histories import ChatMessageHistory
history = ChatMessageHistory()
history.add_user_message("저축을 늘리려면?")
history.add_ai_message("매달 자동이체로 저축하세요.")
# 다음 질문 때 history.messages 를 넘기면 됨

(부분 완성) 3단계 — RunnableWithMessageHistory(6.3): 체인을 감싸 자동 저장·불러오기. 아직 동작하지만 공식적으로 은퇴 예고된 부품이다(2.0에서 제거 예정) — 요즘 공식 권장은 6장에서 배울 LangGraph의 체크포인터(대화 저장소)다. 여기서는 개념 이해용으로만 보자.

from langchain_core.runnables.history import RunnableWithMessageHistory
chain_auto = RunnableWithMessageHistory(
    chain,
    # 대화방 이름표(session_id)를 받아 그 방의 메모지(저장된 대화)를 찾아 주는 함수
    lambda session_id: history,
    # 아래 두 줄: 새 질문이 담긴 키 이름 / 옛 대화가 프롬프트의 어느 빈칸에 들어갈지
    input_messages_key="input",
    history_messages_key="chat_history",
)
chain_auto.invoke(
    {"input": "저축을 늘리려면?"},
    {"configurable": {"session_id": "____"}},  # 대화방을 구분하는 표
)

빈칸: session_id에는 대화방마다 다른 이름을 준다(예: "session_1"). 같은 이름이면 같은 메모지를 공유한다.

(독립 적용) 4단계 — 대화가 100턴 넘게 길어지면? 두 가지로 줄인다.

from langchain_core.messages import trim_messages
# 트리밍 — 오래된 대화를 잘라 내고 최근 것만 남긴다
# 이름은 max_tokens지만 여기선 "메시지 2개"라는 뜻 — token_counter=len 이 개수를 세기 때문
trimmer = trim_messages(strategy="last", max_tokens=2, token_counter=len)
# 요약 — 옛 대화를 압축해 핵심만 남김 (요약 프롬프트로 모델이 줄임)

미니 시나리오 — 상담 챗봇이 너무 느리고 토큰비가 많이 나온다.

트리밍은 빠르지만 오래된 맥락을 잃는다. 요약은 토큰을 크게 아끼지만 중요 정보가 빠질 위험이 있다.

초반 맥락이 중요하면 요약을, 속도가 중요하면 트리밍을 고른다. 둘은 트레이드오프다.

한 걸음 더 ▸ (지금 몰라도 됨) — 요즘 권장은 대화 상태를 langgraph로 관리하는 쪽으로 옮겨 가고 있다. 개념은 이 절로 충분하다. 신규 코드를 짤 땐 공식 문서의 최신 권장을 확인하면 된다.


단순 규칙 (헷갈리면 이대로)

  • 모델은 직접 호출하지 말고 체인으로: prompt | model | parser. 나중에 교체가 쉽다.
  • 일관된 답이 필요하면 temperature=0, 창의적이면 높인다.
  • 구조화된 답이 필요하면 출력 파서(또는 가능하면 함수·도구 호출).
  • 챗봇의 대화 기억은 LangGraph 체크포인터(6장)가 현행 공식 권장. (RunnableWithMessageHistory는 은퇴 예고 상태 — 기존 코드 읽을 때만 알아두자.)
  • 대화가 길어지면 트리밍이나 요약으로 줄인다.
  • 모델·가격은 공식 페이지에서 확인 — 책 표기는 2025년 예시다.

정리 (핵심 3줄)

랭체인은 모델·프롬프트·파서·메모리를 같은 규격의 블록(러너블)으로 감싸, |(LCEL)로 한 줄에 잇게 해 주는 부품 상자다.

덕분에 모델 교체는 블록 하나 갈아 끼우기로, 복잡한 흐름은 짧은 한 줄로 줄어든다.

이 여섯 블록(생태계·모델·LCEL·프롬프트·파서·메모리)이 다음에 만들 RAG의 바탕이 된다.


더 해보기

다음 2장에서는 이 블록들을 진짜로 끼워, 문서를 읽어 답하는 첫 RAG를 직접 만든다. (지금 6블록만 손에 있으면 충분합니다.)


연습문제

  1. 설명. 랭체인이라는 레고 상자의 핵심을 처음 듣는 사람에게 한 문장으로 설명하라.
  2. 구분. 두 개념(LCEL 체인, 직접 호출)을 실제 예시 하나로 구분하라.
  3. 적용. 내 프로젝트나 학습 노트에서 이 장의 개념을 적용해 작게 개선할 지점을 하나 고르라.

부록 A. 쉬운 용어 사전

한글 용어 원문 영문명 아주 쉬운 뜻 이 장에서 나온 위치
LCEL 체인 - 랭체인 부품을 |로 이어 하나의 처리 흐름처럼 만든 것. 부록 B와 본문 예시
직접 호출 - 체인이나 공통 규격 없이 모델 API나 함수를 바로 부르는 방식. 부록 B와 본문 예시
프롬프트 템플릿 Prompt Template 매번 달라지는 입력을 빈칸에 넣어 같은 질문 양식을 만드는 틀. 부록 B와 본문 예시
출력 파서 Output Parser 모델 답을 표, JSON, 목록처럼 쓰기 쉬운 모양으로 정리하는 부품. 부록 B와 본문 예시
## 부록 B. 헷갈리는 개념 비교표
A B 구분 포인트
LCEL 체인 직접 호출 체인은 부품을 바꾸기 쉽고, 직접 호출은 빠르지만 흐름이 흩어진다.
프롬프트 템플릿 출력 파서 앞은 질문 양식, 뒤는 답변 모양 정리 담당이다.

부록 C. 연습문제 풀이

  1. 설명 예시. 랭체인이라는 레고 상자는 RAG에서 자료를 더 잘 찾고, 근거를 더 안전하게 붙이고, 답변 흐름을 더 다루기 쉽게 만드는 방법을 보는 장이다. 중요한 것은 용어를 외우는 것이 아니라, 이 개념이 어떤 입력·부품·결정에 영향을 주는지 말로 풀어 보는 것이다.
  2. 구분 예시. 두 개념(LCEL 체인, 직접 호출)의 차이는 이렇게 잡으면 된다. 체인은 부품을 바꾸기 쉽고, 직접 호출은 빠르지만 흐름이 흩어진다. 실제 사례를 볼 때는 목적, 입력, 실패했을 때의 증상을 따로 적어 보면 헷갈리지 않는다.
  3. 적용 예시. 가장 작은 개선부터 고른다. 예를 들어 이름을 더 분명히 하거나, 평가 기준을 한 줄 추가하거나, 직접 알 필요 없는 내부 정보를 감추는 식으로 시작한다. 한 번에 크게 갈아엎는 것보다 작은 변경 하나를 확인하며 진행하는 쪽이 입문 단계에 맞다.
난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플   P 음성 버전 2026-08-05b